package com.autonavi.yunda.yunji.core.engine.javascript;

import com.autonavi.yunda.yunji.common.utils.JsonUtils;
import com.autonavi.yunda.yunji.core.vo.function.FunctionClassProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
import org.graalvm.polyglot.proxy.ProxyArray;
import org.graalvm.polyglot.proxy.ProxyHashMap;

import java.net.URL;
import java.util.*;

import static com.autonavi.yunda.yunji.core.utils.FileUtil.parseJarFolderToTempFolder;


@Slf4j
public class JsExecutor {
    /**
     * used for javascript ast parser
     */
    private static final String astScript = "" +
            "let acorn = require(\"acorn\")\n" +
            "function astParser(source){\n" +
            "    var ast = acorn.parse(source,  {ecmaVersion: 2021})\n" +
            "    out = []\n" +
            "    for(const n of ast.body){\n" +
            "        switch (n.type) {\n" +
            "            case 'FunctionDeclaration': \n" +
            "            out.push({\n" +
            "                type: \"function\",\n" +
            "                name: n.id.name,\n" +
            "                start: n.start,\n" +
            "                end: n.end,\n" +
            "                innerStart: n.body.start,\n" +
            "                innerEnd: n.body.end\n" +
            "            }\n" +
            "            );\n" +
            "            case 'ExpressionStatemen': \n" +
            "            out.push({\n" +
            "                type: \"expression\",\n" +
            "                start: n.start,\n" +
            "                end: n.end\n" +
            "            });\n" +
            "            case 'VariableDeclaration': \n" +
            "            if(n.declarations != null){\n" +
            "                out.push({\n" +
            "                    type: \"var\",\n" +
            "                    name: n.declarations[0].id.name,\n" +
            "                    start: n.start,\n" +
            "                    end: n.end,\n" +
            "                    innerType: n.declarations[0].init.type,\n" +
            "                    innerStart: n.declarations[0].init.start,\n" +
            "                    innerEnd: n.declarations[0].init.end\n" +
            "                })\n" +
            "            }\n" +
            "        }       \n" +
            "    }\n" +
            "    return out\n" +
            "}\n" +
            "astParser";


    private static Value astFunction = null;

    private static synchronized Value initAstFunction() {
        if (astFunction == null) {
            URL url = JsExecutor.class.getClassLoader().getResource("node_modules");
            Map<String, String> options = new HashMap<>(8);
            options.put("js.commonjs-require", "true");
            if (url != null) {
                if (url.getProtocol().equals("jar")) {
                    String tmpDir = parseJarFolderToTempFolder(url, "node_modules");
                    options.put("js.commonjs-require-cwd", tmpDir + "/node_modules");
                } else {
                    options.put("js.commonjs-require-cwd", url.getPath());
                }
            } else {
                log.warn("node_modules not found");
            }
            Context context = Context.newBuilder("js")
                    .allowExperimentalOptions(true)
                    .allowIO(true)
                    .options(options)
                    .build();
            astFunction = context.eval("js", astScript);
        }
        return astFunction;
    }


    private synchronized static List<Map<?, ?>> astScript(String script) {
        Value astFunction = initAstFunction();
        return astFunction.execute(script).as(List.class);
    }


    private static List<AstNode> functionNodes(String script) {
        List<AstNode> nodes = astParser(script);
        List<AstNode> functions = new ArrayList<>();
        for (AstNode node : nodes) {
            if ("function".equals(node.type)) {
                functions.add(node);
            }
        }
        return functions;
    }

    /**
     * 脚本执行入口，用于测试情况下，单次执行使用
     *
     * @param script 测试脚本
     * @param args   请求参数
     * @return 执行结果
     */
    public static Object doExec(String script, Map<String, Object> args) {
        JSFunctionContext context = parserContext(script, args.keySet());
        return doExec(context, args);
    }

    /**
     * 根据脚本，分析脚本执行上下文，脚本上下文可以被缓存，用于运行时复用执行
     *
     * @param script     执行脚本
     * @param paramNames 参数列表
     * @return 执行结果
     */
    public static JSFunctionContext parserContext(String script, Set<String> paramNames) {
        List<AstNode> functions = functionNodes(script);
        StringBuilder execScript = new StringBuilder();
        for (AstNode fuc : functions) {
            if ("process".equals(fuc.name)) {
                execScript.append("function process( ").append(String.join(", ", paramNames)).append(")").append(fuc.content);
            } else {
                execScript.append(fuc.content);
            }
        }
        execScript.append("\n").append("process");
        log.debug("js execScript = {}", execScript);
        Context context = Context.newBuilder("js")
                .allowExperimentalOptions(true)
                .allowIO(true)
                .allowHostAccess(HostAccess.newBuilder().allowListAccess(true).build())
                .option("js.experimental-foreign-object-prototype", "true")
                .build();

        Value funcExec = context.eval("js", execScript.toString());

        return new JSFunctionContext(funcExec, paramNames, script);
    }

    private static Object proxyObject(Object inObj) {

        if (inObj instanceof Map) {
            HashMap<Object, Object> proxy = new HashMap<>(8);
            for (Map.Entry<?, ?> e : ((Map<?, ?>) inObj).entrySet()) {
                if (e.getValue() instanceof Map || e.getValue() instanceof List) {
                    proxy.put(e.getKey(), proxyObject(e.getValue()));
                } else {
                    proxy.put(e.getKey(), e.getValue());
                }
            }
            return ProxyHashMap.from(proxy);
        } else if (inObj instanceof List) {
            List<Object> proxy = new ArrayList<>();
            for (Object obj : (List<?>) inObj) {
                if (obj instanceof Map || obj instanceof List) {
                    proxy.add(proxyObject(obj));
                } else {
                    proxy.add(obj);
                }
            }
            return ProxyArray.fromList(proxy);
        } else {
            return inObj;
        }

    }


    /**
     * 运行时执行入口
     *
     * @param context 脚本上下文环境 {@link JsExecutor#parserContext(String, Set)}
     * @param params  实际执行时参数列表
     * @return 执行结果
     */
    public static Object doExec(JSFunctionContext context, Map<String, Object> params) {

        List<Object> execArgs = new ArrayList<>();
        for (String paramName : context.getParamsNames()) {
            Object val = params.get(paramName);
            if (val instanceof Map || val instanceof List) {
                val = proxyObject(val);
            }
            execArgs.add(val);
        }
        Value rst = context.getFunction().execute(execArgs.toArray());
        if (rst.hasArrayElements()) {
            return rst.as(List.class);
        } else if (rst.hasMembers()) {
            return rst.as(Map.class);
        } else if (rst.isNumber()) {
            if (rst.fitsInInt()) {
                return rst.asInt();
            } else if (rst.fitsInDouble()) {
                return rst.asDouble();
            }
            return rst;
        } else if (rst.isString()) {
            return rst.asString();
        } else if (rst.isBoolean()) {
            return rst.asBoolean();
        } else {
            return rst.as(Map.class);
        }
    }

    /**
     * 根据提供的参数列表及脚本，生成合并后的脚本；合并过程为，参数 + 脚本的function
     *
     * @param args   参数列表
     * @param script 脚本
     * @return 合并后的脚本
     */
    public String mergeProperties(Map<String, Object> args, String script) {
        StringBuilder builder = new StringBuilder();
        List<AstNode> functions = functionNodes(script);
        for (Map.Entry<String, Object> arg : args.entrySet()) {
            Object value = arg.getValue();
            if (value instanceof Integer || value instanceof Float || value instanceof Double) {
                builder.append("const ").append(arg.getKey()).append(" = ").append(arg.getValue()).append("\n\n");
            } else if (value instanceof String) {
                builder.append("const ").append(arg.getKey()).append(" = ").append("'").append(arg.getValue()).append("'").append("\n\n");
            } else {
                builder.append("const ").append(arg.getKey()).append(" = ").append(JsonUtils.toString(value)).append("\n\n");
            }
        }

        boolean process = false;
        for (AstNode function : functions) {
            if ("process".equals(function.name)) {
                process = true;
                builder.append("function process() ").append(function.content).append("\n").append("\n");
            } else {
                builder.append(function.content).append("\n");
            }
        }
        if (!process) {
            builder.append("\nfunction process() {\n\n}\n\n");
        }
        return builder.toString();
    }

    private static List<AstNode> astParser(String script) {
        List<Map<?, ?>> rst = astScript(script);
        List<AstNode> astNodes = new ArrayList<>();
        for (Map<?, ?> e : rst) {
            String type = (String) e.get("type");
            if ("var".equals(type)) {
                AstNode node = new AstNode((String) e.get("type"), (Integer) e.get("start"), (Integer) e.get("end"), (Integer) e.get("innerStart"), (Integer) e.get("innerEnd"), (String) e.get("name"), (String) e.get("innerType"));
                node.content = script.substring(node.start, node.end);

                node.debugValue = script.substring(node.innerStart, node.end).replaceAll("[;]+$", "");
                astNodes.add(node);

            } else if ("function".equals(type)) {
                AstNode node = new AstNode((String) e.get("type"), (Integer) e.get("start"), (Integer) e.get("end"), (Integer) e.get("innerStart"), (Integer) e.get("innerEnd"), (String) e.get("name"));
                if ("process".equals(node.name)) {
                    node.content = script.substring(node.innerStart, node.end);
                } else {
                    node.content = script.substring(node.start, node.end);
                }
                astNodes.add(node);
            }
        }
        return astNodes;
    }


    /**
     * 合并script参数，返回 const + function
     */
    public String mergeScript(List<FunctionClassProperty> properties) throws JsonProcessingException {
        StringBuilder builder = new StringBuilder();

        for (FunctionClassProperty p : properties) {
            builder.append("const ").append(p.getName()).append(" = ").append(
                    p.getDebugValue()).append(";\n");
        }

        builder.append("\nfunction process() {\n\treturn {\n");

        int i = 0;
        for (FunctionClassProperty p : properties) {
            if (i != 0) {
                builder.append(",\n");
            }
            builder.append("\t\t").append(p.getName()).append(": ").append(p.getName());
            i++;
        }
        builder.append("\n\t};\n}\n");
        return builder.toString();
    }
}
